import os
import sys
import shutil
import tempfile
import subprocess
import threading
import socket
import re
from tkinter import Tk, Label, Button, filedialog, StringVar, Frame, CENTER, BOTH, TOP, LEFT, X, W, FLAT, Entry, Toplevel
from tkinter import font as tkfont
from tkinter import PhotoImage
from tkinter import NSEW
from tkinter import E
from tkinter import N, S
from tkinter import NW, NE, SW, SE
from tkinter import SUNKEN
from tkinter import GROOVE
from tkinter import RAISED
from tkinter import FLAT
from tkinter import RIDGE
from tkinter import ttk
from PIL import Image, ImageTk
import qrcode
from flask import Flask, request, render_template_string
import requests
import time
import io
try:
    import win32clipboard
except ImportError:
    win32clipboard = None

# ========== 配置 ==========
UPLOAD_FOLDER = os.path.join(os.path.expanduser("~"), "Desktop")
PORT = 5000  # Flask 端口

# ========== 新增依赖 ==========
try:
    from aiortc import RTCPeerConnection, RTCSessionDescription, RTCDataChannel
    import asyncio
except ImportError:
    RTCPeerConnection = None  # 未安装aiortc
    asyncio = None

import uuid
import json

# ========== P2P信令会话存储 ==========
p2p_sessions = {}

# ========== Flask 服务器 ==========
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

# ========== P2P相关路由和类 ==========
P2P_HTML = '''
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <title>P2P直连传文件</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <style>
    body { font-family: '微软雅黑', Arial, sans-serif; background: #f7f7f7; margin: 0; }
    .container { max-width: 400px; margin: 6vh auto; background: #fff; border-radius: 12px; box-shadow: 0 2px 8px #ccc; padding: 6vw 4vw 4vw 4vw; text-align: center; }
    h2 { color: #222; font-size: 1.3em; margin-bottom: 1.2em; }
    .file-label { display: inline-block; background: #4CAF50; color: #fff; padding: 12px 24px; border-radius: 8px; font-size: 1em; cursor: pointer; margin-bottom: 10px; margin-top: 10px; transition: background 0.2s; }
    .file-label:hover { background: #388E3C; }
    input[type=file] { display: none; }
    .file-name { color: #666; font-size: 0.95em; margin-bottom: 10px; margin-top: 4px; word-break: break-all; }
    .upload-btn { background: #388E3C; color: white; border: none; padding: 12px 0; border-radius: 8px; font-size: 1.1em; width: 60%; max-width: 220px; cursor: pointer; margin-top: 10px; box-shadow: 0 1px 3px #eee; transition: background 0.2s; display: block; margin-left: auto; margin-right: auto; }
    .upload-btn:active { background: #256d27; }
    .progress-bar-wrap { width: 100%; background: #eee; height: 10px; border-radius: 5px; margin-top: 10px; display: none; }
    .progress-bar { height: 10px; width: 0; background: #4CAF50; border-radius: 5px; transition: width 0.2s; }
    .success { color: #388E3C; margin-top: 18px; font-size: 1.1em; }
    .error { color: #E53935; margin-top: 18px; font-size: 1.1em; }
  </style>
</head>
<body>
  <div class="container">
    <h2>P2P直连传文件</h2>
    <form id="p2pForm" onsubmit="sendFile(event)">
      <label class="file-label">
        选择文件
        <input type="file" id="fileInput" name="file" multiple onchange="showFileName(this)">
      </label>
      <div class="file-name" id="fileName"></div>
      <button type="submit" class="upload-btn">P2P发送</button>
      <div class="progress-bar-wrap" id="progressBarWrap">
        <div class="progress-bar" id="progressBar"></div>
      </div>
      <div class="success" id="successMsg"></div>
      <div class="error" id="errorMsg"></div>
      <div class="speed" id="speedMsg"></div>
      <div class="status" id="statusMsg" style="color:#1976d2;margin-top:10px;font-size:1em;"></div>
    </form>
  </div>
  <script>
    let session = new URLSearchParams(window.location.search).get('session');
    let pc = null;
    let dc = null;
    let fileBuffer = null;
    let fileName = '';
    let fileSize = 0;
    let progressBar = document.getElementById('progressBar');
    let progressBarWrap = document.getElementById('progressBarWrap');
    let successMsg = document.getElementById('successMsg');
    let errorMsg = document.getElementById('errorMsg');
    let startTime = Date.now();
    let lastTime = startTime;
    let lastOffset = 0;
    let statusMsg = document.getElementById('statusMsg');

    function showFileName(input) {
      if (input.files.length) {
        fileName = input.files[0].name;
        fileSize = input.files[0].size;
        document.getElementById('fileName').innerText = fileName + ' (' + fileSize + '字节)';
      }
    }
    async function sendFile(e) {
      e.preventDefault();
      errorMsg.innerText = '';
      successMsg.innerText = '';
      statusMsg.innerText = '正在发起P2P连接...';

      let fileInput = document.getElementById('fileInput');
      if (!fileInput.files.length) return;
      let files = Array.from(fileInput.files);

      // 1. 创建WebRTC连接
      let pc = new RTCPeerConnection();
      let dc = pc.createDataChannel('file');
      dc.binaryType = 'arraybuffer';

      dc.onopen = async function() {
        statusMsg.innerText = 'P2P通道已建立，开始发送文件...';
        for (let i = 0; i < files.length; i++) {
          let file = files[i];
          let fileBuffer = await file.arrayBuffer();
          // 发送文件名和大小
          dc.send(JSON.stringify({name: file.name, size: file.size}));
          // 分片发送
          let chunkSize = 16 * 1024;
          let offset = 0;
          while (offset < fileBuffer.byteLength) {
            let chunk = fileBuffer.slice(offset, offset + chunkSize);
            dc.send(chunk);
            offset += chunkSize;
            progressBar.style.width = (offset / fileBuffer.byteLength * 100) + '%';
            progressBarWrap.style.display = 'block';
            await new Promise(r => setTimeout(r, 0));
          }
          // 单个文件发送完成提示
          successMsg.innerText = `已发送：${file.name}`;
          progressBar.style.width = '100%';
          await new Promise(r => setTimeout(r, 500)); // 稍作停顿
          progressBar.style.width = '0%';
        }
        successMsg.innerText = '全部文件发送完成！';
        statusMsg.innerText = '全部文件已通过P2P发送。';
      };

      dc.onerror = function(e) { errorMsg.innerText = 'DataChannel错误: ' + e; };

      // 2. 信令交换
      statusMsg.innerText = '正在生成offer...';
      let offer = await pc.createOffer();
      await pc.setLocalDescription(offer);

      statusMsg.innerText = '正在通知桌面端，等待响应...';
      await fetch('/signal/' + session, {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({offer: offer})});

      let answer = null;
      statusMsg.innerText = '等待桌面端协商...';
      for (let i = 0; i < 30; i++) {
        let resp = await fetch('/signal/' + session);
        let data = await resp.json();
        if (data.answer) { answer = data.answer; break; }
        await new Promise(r => setTimeout(r, 500));
      }
      if (!answer) {
        errorMsg.innerText = 'P2P协商超时';
        statusMsg.innerText = 'P2P协商失败，请重试';
        return;
      }
      statusMsg.innerText = '协商成功，建立P2P通道...';
      await pc.setRemoteDescription(answer);
    }
  </script>
</body>
</html>
'''

@app.route('/p2p')
def p2p_page():
    return P2P_HTML

@app.route('/signal/<session_id>', methods=['POST', 'GET'])
def p2p_signal(session_id):
    if request.method == 'POST':
        p2p_sessions[session_id] = request.json
        return {'status': 'ok'}
    else:
        return p2p_sessions.get(session_id, {})

# ========== aiortc WebRTC服务端 ==========
class P2PReceiver:
    def __init__(self, session_id, save_dir, status_callback=None):
        self.session_id = session_id
        self.save_dir = save_dir
        self.status_callback = status_callback
        self.loop = asyncio.new_event_loop()
        self.thread = threading.Thread(target=self.run, daemon=True)
        self.thread.start()

    def run(self):
        asyncio.set_event_loop(self.loop)
        self.loop.run_until_complete(self.start())

    async def start(self):
        total_wait_time = 600  # 10分钟
        interval = 0.5
        last_activity = time.time()
        if self.status_callback:
            self.status_callback("等待网页端发起P2P协商...", "#1976d2")
        while time.time() - last_activity < total_wait_time:
            wait_seconds = 300  # 单次协商最多5分钟
            max_tries = int(wait_seconds / interval)
            offer = None
            for i in range(max_tries):
                offer = p2p_sessions.get(self.session_id, {}).get('offer')
                if offer:
                    break
                remaining = int(total_wait_time - (time.time() - last_activity))
                if self.status_callback:
                    self.status_callback(f"等待手机扫码协商...（剩余{remaining}秒）", "#1976d2")
                await asyncio.sleep(interval)
            if not offer:
                if self.status_callback:
                    self.status_callback("P2P协商超时", "#E53935")
                return
            if self.status_callback:
                self.status_callback("P2P协商成功，等待文件...", "#43b984")
            pc = RTCPeerConnection()
            file_info = {}
            file_data = bytearray()
            @pc.on('datachannel')
            def on_datachannel(channel):
                @channel.on('message')
                def on_message(message):
                    nonlocal file_info, file_data, last_activity
                    if isinstance(message, str):
                        try:
                            file_info = json.loads(message)
                            file_data.clear()
                        except Exception:
                            pass
                    elif isinstance(message, bytes) or isinstance(message, bytearray):
                        file_data.extend(message)
                        if file_info and len(file_data) >= file_info.get('size', 0):
                            # 保存文件
                            fname = file_info.get('name', 'p2p_recv')
                            save_path = os.path.join(self.save_dir, fname)
                            with open(save_path, 'wb') as f:
                                f.write(file_data)
                            print(f"[P2P] 文件已保存: {save_path}")
                            file_info = {}
                            file_data.clear()
                            last_activity = time.time()
            await pc.setRemoteDescription(RTCSessionDescription(sdp=offer['sdp'], type=offer['type']))
            answer = await pc.createAnswer()
            await pc.setLocalDescription(answer)
            # 回传answer
            p2p_sessions[self.session_id]['answer'] = {'sdp': pc.localDescription.sdp, 'type': pc.localDescription.type}
            # 等待文件传输完成或DataChannel关闭
            await asyncio.sleep(60)
            await pc.close()
            # 清理本次offer/answer，便于下次新协商
            p2p_sessions[self.session_id].pop('offer', None)
            p2p_sessions[self.session_id].pop('answer', None)
        if self.status_callback:
            self.status_callback("P2P监听已结束，如需继续请重新扫码", "#E53935")

def get_data_dir():
    """获取或创建在AppData/Local下的专属应用数据目录"""
    app_name = "QRFileTransfer"
    if sys.platform == "win32":
        base_dir = os.environ.get('LOCALAPPDATA', os.path.expanduser('~'))
    else: # For macOS and Linux
        base_dir = os.path.join(os.path.expanduser('~'), '.config')
    
    data_dir = os.path.join(base_dir, app_name)
    
    try:
        os.makedirs(data_dir, exist_ok=True)
    except OSError:
        # 如果AppData目录不可写，则回退到用户主目录下的隐藏文件夹
        data_dir = os.path.join(os.path.expanduser('~'), f'.{app_name}')
        os.makedirs(data_dir, exist_ok=True)
        
    return data_dir

def get_data_file_path(filename):
    """获取数据文件在专属数据目录中的完整路径"""
    return os.path.join(get_data_dir(), filename)

def ensure_cpolar_in_data_dir():
    """确保cpolar.exe存在于AppData目录中，如果不存在则从资源中复制。"""
    data_cpolar_path = get_data_file_path('cpolar.exe')
    if not os.path.exists(data_cpolar_path):
        if hasattr(sys, '_MEIPASS'):
            src = os.path.join(sys._MEIPASS, 'cpolar.exe')
        else:
            src = os.path.join(os.path.dirname(__file__), 'cpolar.exe')
        if os.path.exists(src):
            shutil.copyfile(src, data_cpolar_path)
        else:
            return None
    return data_cpolar_path

def ensure_tokens_file_in_data_dir():
    """确保tokens.txt存在于AppData目录中，如果不存在则从打包资源中复制。"""
    tokens_path = get_data_file_path('tokens.txt')
    if not os.path.exists(tokens_path):
        if hasattr(sys, '_MEIPASS'):
            src = os.path.join(sys._MEIPASS, 'tokens.txt')
        else:
            src = os.path.join(os.path.dirname(__file__), 'tokens.txt')
        
        if os.path.exists(src):
            try:
                shutil.copyfile(src, tokens_path)
            except Exception as e:
                print(f"无法复制 tokens.txt: {e}")

def auto_update_tokens_from_url():
    url = "https://gitee.com/wjcai110/upload-tool-tokens/raw/master/tokens.txt"  # 请替换为你的Gitee仓库raw直链
    tokens_file = get_data_file_path('tokens.txt')
    try:
        resp = requests.get(url, timeout=5)
        if resp.status_code == 200 and resp.text.strip():
            # 清理每行的前后空格和空行
            cleaned_lines = []
            for line in resp.text.split('\n'):
                line = line.strip()  # 去除前后空格
                if line:  # 只保留非空行
                    cleaned_lines.append(line)
            
            # 保存清理后的内容
            with open(tokens_file, 'w', encoding='utf-8') as f:
                for line in cleaned_lines:
                    f.write(line + '\n')
            print("已自动更新 tokens.txt")
    except Exception as e:
        print("自动更新 tokens.txt 失败：", e)

class SettingsWindow(Toplevel):
    def __init__(self, master_app):
        super().__init__(master_app.master)
        self.title("设置")
        self.geometry("420x220")
        self.resizable(False, False)
        self.transient(master_app.master)
        self.grab_set()
        self.configure(bg="#f7f7f7")
        self.master_app = master_app
        self.authtoken_path = get_data_file_path('authtoken.txt')

        # 居中弹窗
        self.update_idletasks()
        w, h = 420, 220
        ws = self.winfo_screenwidth()
        hs = self.winfo_screenheight()
        x = (ws // 2) - (w // 2)
        y = (hs // 2) - (h // 2)
        self.geometry(f"{w}x{h}+{x}+{y}")

        # 主Frame带圆角和阴影
        outer = Frame(self, bg="#e0e0e0", bd=0, relief="flat")
        outer.place(relx=0, rely=0, relwidth=1, relheight=1)
        main_frame = Frame(outer, bg="#fff", bd=0, relief="flat", highlightbackground="#bdbdbd", highlightthickness=1)
        main_frame.place(relx=0.04, rely=0.04, relwidth=0.92, relheight=0.92)

        Label(main_frame, text="高级设置", font=("微软雅黑", 15, "bold"), fg="#222", bg="#fff").pack(anchor=W, pady=(8, 0), padx=18)
        Label(main_frame, text="请粘贴 cpolar authtoken（如用token池可留空）", font=("微软雅黑", 9), fg="#888", bg="#fff").pack(anchor=W, pady=(0, 8), padx=18)

        self.token_var = StringVar()
        self.token_entry = Entry(main_frame, textvariable=self.token_var, font=("微软雅黑", 11), bd=1, relief="flat", bg="#f7f7f7", highlightthickness=1, highlightbackground="#bdbdbd", highlightcolor="#4CAF50")
        self.token_entry.pack(fill=X, padx=18, pady=(0, 12), ipady=6)
        self.token_entry.bind('<Return>', lambda e: self.save_token())
        self.token_entry.focus_set()
        self.load_token()

        btn_frame = Frame(main_frame, bg="#fff")
        btn_frame.pack(pady=8)
        self.save_btn = Button(btn_frame, text="保存", command=self.save_token, font=("微软雅黑", 10, "bold"), bg="#4CAF50", fg="#fff", activebackground="#388E3C", activeforeground="#fff", bd=0, relief=FLAT, padx=18, pady=4, cursor="hand2")
        self.save_btn.pack(side=LEFT, padx=8)
        self.cancel_btn = Button(btn_frame, text="取消", command=self.destroy, font=("微软雅黑", 10), bg="#BDBDBD", fg="#fff", activebackground="#888", activeforeground="#fff", bd=0, relief=FLAT, padx=18, pady=4, cursor="hand2")
        self.cancel_btn.pack(side=LEFT, padx=8)

    def load_token(self):
        try:
            with open(self.authtoken_path, 'r', encoding='utf-8') as f:
                self.token_var.set(f.read().strip())
        except FileNotFoundError:
            pass

    def save_token(self):
        token = self.token_var.get().strip()
        if token:
            try:
                with open(self.authtoken_path, 'w', encoding='utf-8') as f:
                    f.write(token)
                self.master_app.show_tip("Authtoken 已保存!")
                self.after(500, self.destroy)
            except Exception as e:
                self.master_app.show_tip(f"保存失败: {e}")
        else:
            self.master_app.show_tip("Authtoken 不能为空!")

class CpolarManager:
    def __init__(self):
        self.process = None
        self.public_url = None
        self._stop_event = threading.Event()
        self.cpolar_path = ensure_cpolar_in_data_dir()
        self.thread = None

    def start(self, authtoken):
        if not self.cpolar_path:
            print("错误: cpolar.exe 未找到, 无法启动公网模式。")
            return
        if self.process and self.process.poll() is None:
            return
        self.public_url = None
        self._stop_event.clear()
        cmd = [self.cpolar_path, 'http', '5000', '-authtoken', authtoken, '--region', 'cn', '--log', 'stdout']
        CREATE_NO_WINDOW = 0x08000000  # 只在 Windows 下有效
        self.process = subprocess.Popen(
            cmd,
            cwd=get_data_dir(),
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            universal_newlines=True,
            bufsize=1,
            encoding='utf-8',
            errors='replace',
            creationflags=CREATE_NO_WINDOW if sys.platform == "win32" else 0
        )
        self.thread = threading.Thread(target=self._read_stdout, daemon=True)
        self.thread.start()

    def _read_stdout(self):
        # 匹配cpolar新版输出格式
        url_pattern = re.compile(r"Tunnel established at (https?://[\w\.-]+)")
        for line in self.process.stdout:
            if self._stop_event.is_set():
                break
            match = url_pattern.search(line)
            if match and match.group(1).startswith('https://'):
                self.public_url = match.group(1)
                break

    def stop(self):
        if self.process:
            self._stop_event.set()
            self.process.terminate()
            try:
                self.process.wait(timeout=2)
            except subprocess.TimeoutExpired:
                self.process.kill()
            self.process = None
        self.public_url = None

    def get_public_url(self):
        return self.public_url

# ========== Flask 新增路由 ==========
UPLOAD_HTML = '''
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <title>上传文件到桌面</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <style>
    body { font-family: '微软雅黑', Arial, sans-serif; background: #f7f7f7; margin: 0; }
    .container {
      max-width: 96vw;
      width: 96vw;
      min-width: 0;
      margin: 6vh auto;
      background: #fff;
      border-radius: 12px;
      box-shadow: 0 2px 8px #ccc;
      padding: 6vw 4vw 4vw 4vw;
      text-align: center;
    }
    h2 { color: #333; font-size: 1.3em; margin-bottom: 1.2em; }
    .drop-area {
      border: 2px dashed #4CAF50;
      border-radius: 10px;
      padding: 30px 10px;
      margin-bottom: 18px;
      background: #f9fff9;
      color: #4CAF50;
      font-size: 1.1em;
      cursor: pointer;
      transition: background 0.2s, border-color 0.2s;
    }
    .drop-area.dragover {
      background: #e8f5e9;
      border-color: #388E3C;
      color: #388E3C;
    }
    .file-label {
      display: inline-block;
      background: #4CAF50;
      color: #fff;
      padding: 12px 24px;
      border-radius: 8px;
      font-size: 1em;
      cursor: pointer;
      margin-bottom: 10px;
      margin-top: 10px;
      transition: background 0.2s;
    }
    .file-label:hover { background: #388E3C; }
    input[type=file] {
      display: none;
    }
    .file-name {
      color: #666;
      font-size: 0.95em;
      margin-bottom: 10px;
      margin-top: 4px;
      word-break: break-all;
    }
    .upload-btn {
      background: #388E3C;
      color: white;
      border: none;
      padding: 12px 0;
      border-radius: 8px;
      font-size: 1.1em;
      width: 60%;
      max-width: 220px;
      cursor: pointer;
      margin-top: 10px;
      box-shadow: 0 1px 3px #eee;
      transition: background 0.2s;
      display: block;
      margin-left: auto;
      margin-right: auto;
    }
    .upload-btn:active { background: #256d27; }
    .progress-bar-wrap {
      width: 100%;
      background: #eee;
      height: 10px;
      border-radius: 5px;
      margin-top: 10px;
      display: none;
    }
    .progress-bar {
      height: 10px;
      width: 0;
      background: #4CAF50;
      border-radius: 5px;
      transition: width 0.2s;
    }
    .speed {
      color: #888;
      font-size: 0.95em;
      margin-top: 4px;
      min-height: 1.2em;
    }
    .success { color: #388E3C; margin-top: 18px; font-size: 1.1em; }
    .file-list { color: #333; margin-top: 10px; font-size: 1em; word-break: break-all; }
    @media (min-width: 500px) {
      .container { max-width: 400px; width: 400px; }
    }
  </style>
</head>
<body>
  <div class="container">
    <h2>选择文件上传</h2>
    <div class="drop-area" id="dropArea" onclick="document.getElementById('fileInput').click();">
      拖拽文件到此处，或点击选择文件
    </div>
    <form id="uploadForm" onsubmit="uploadFile(event)">
      <label class="file-label">
        选择文件
        <input type="file" id="fileInput" name="file" multiple onchange="showFileName(this)">
      </label>
      <div class="file-name" id="fileName"></div>
      <button type="submit" class="upload-btn">上传</button>
      <div class="progress-bar-wrap" id="progressBarWrap">
        <div class="progress-bar" id="progressBar"></div>
      </div>
      <div class="speed" id="speed"></div>
    </form>
    {% if filenames %}
      <div class="success">上传成功:</div>
      <div class="file-list">
        {% for fn in filenames %}
          <div>{{ fn }}</div>
        {% endfor %}
      </div>
    {% endif %}
  </div>
  <script>
    var dropArea = document.getElementById('dropArea');
    var fileInput = document.getElementById('fileInput');
    var fileList = [];
    dropArea.addEventListener('dragover', function(e) {
      e.preventDefault();
      dropArea.classList.add('dragover');
    });
    dropArea.addEventListener('dragleave', function(e) {
      e.preventDefault();
      dropArea.classList.remove('dragover');
    });
    dropArea.addEventListener('drop', function(e) {
      e.preventDefault();
      dropArea.classList.remove('dragover');
      var files = e.dataTransfer.files;
      if (files.length) {
        fileInput.files = files;
        showFileName(fileInput);
      }
    });
    function showFileName(input) {
      var names = [];
      for (var i = 0; i < input.files.length; i++) {
        names.push(input.files[i].name);
      }
      document.getElementById('fileName').innerText = names.join(', ');
    }
    function uploadFile(e) {
      e.preventDefault();
      var input = document.getElementById('fileInput');
      var files = input.files;
      if (!files.length) return;
      var formData = new FormData();
      for (var i = 0; i < files.length; i++) {
        formData.append('file', files[i]);
      }
      var xhr = new XMLHttpRequest();
      xhr.open('POST', '/', true);
      var progressBar = document.getElementById('progressBar');
      var progressBarWrap = document.getElementById('progressBarWrap');
      var speedDiv = document.getElementById('speed');
      progressBarWrap.style.display = 'block';
      var startTime = Date.now();
      xhr.upload.onprogress = function(e) {
        if (e.lengthComputable) {
          var percent = e.loaded / e.total * 100;
          progressBar.style.width = percent + '%';
          var elapsed = (Date.now() - startTime) / 1000;
          var speed = e.loaded / 1024 / elapsed; // KB/s
          speedDiv.innerText = '速度: ' + speed.toFixed(1) + ' KB/s';
        }
      };
      xhr.onload = function() {
        progressBar.style.width = '100%';
        speedDiv.innerText = '上传完成';
      };
      xhr.onerror = function() {
        speedDiv.innerText = '上传失败，请重试';
      };
      xhr.send(formData);
    }
  </script>
</body>
</html>
'''

uploaded_filenames = []

def run_flask():
    app.run(host='0.0.0.0', port=PORT, debug=False, use_reloader=False)

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    global uploaded_filenames
    filenames = []
    if request.method == 'POST':
        files = request.files.getlist('file')
        for f in files:
            if f and f.filename:
                filename = f.filename
                save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
                f.save(save_path)
                filenames.append(filename)
        if filenames:
            uploaded_filenames = filenames.copy()
    return render_template_string(UPLOAD_HTML, filenames=filenames)

# ========== 获取本机IP ==========
def get_local_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(('8.8.8.8', 80))
        ip = s.getsockname()[0]
    except Exception:
        ip = '127.0.0.1'
    finally:
        s.close()
    return ip

# ========== Tkinter 主界面 ==========
class QRFileTransferApp:
    def __init__(self, master):
        self.master = master
        master.title("二维码传文件到本地")
        master.configure(bg="#f9f9fb")
        master.geometry("500x700")
        master.resizable(False, False)
        master.minsize(500, 700)  # 限制最小宽度和高度

        self.save_path = StringVar()
        initial_path = self.load_save_path()
        self.save_path.set(initial_path)
        app.config['UPLOAD_FOLDER'] = initial_path
        self.upload_status = StringVar()
        self.upload_status.set("等待上传...")
        self.uploaded_files = []
        self.qr_mode = StringVar()
        self.qr_mode.set("本地局域网")
        self.current_qr_url = StringVar()
        self.current_qr_url.set("")
        self.tip_var = StringVar()
        # 顶部提示条美化
        self.tip_label = Label(master, textvariable=self.tip_var, font=('Microsoft YaHei', 10), fg='#3a7afe', bg='#eaf3ff', bd=0, relief='flat', padx=14, pady=4)
        self.tip_label.place(relx=0.5, rely=0.01, anchor='n')
        self.tip_label.configure(highlightbackground="#e0e0e0", highlightthickness=0)
        self.tip_label.after(100, lambda: self.tip_label.config(width=54))

        self.qr_img = None
        self.upload_history = []
        self.last_history = []
        self.cpolar_mgr = CpolarManager()
        self.token_pool = self.load_token_pool()
        self.token_index = 0
        self.last_token_error_time = 0
        self.title_click_count = 0
        self.title_last_click_time = 0
        self.p2p_status_active = False
        self.p2p_status_timer = None
        self.p2p_timer_id = None
        self.p2p_countdown = 0

        # 1. 顶部状态栏（master）
        self.status_label = Label(self.master, textvariable=self.upload_status, font=('Microsoft YaHei', 13, 'bold'), fg="#1976d2", bg="#eaf3ff", bd=0, relief='flat', anchor='center', padx=0, pady=6)
        self.status_label.place(relx=0.5, rely=0.0, anchor='n', relwidth=1.0, y=0)

        # 2. 主内容卡片
        card = Frame(master, bg="#fff", bd=0, relief="flat", highlightbackground="#e0e0e0", highlightthickness=1)
        card.place(relx=0.5, rely=0.06, anchor='n', width=440, height=540)
        card.configure(borderwidth=0)
        card.after(100, lambda: card.config(highlightbackground="#e0e0e0"))
        self.card = card  # 保存引用，便于定位

        # 标题
        title_label = Label(card, text="二维码传文件", font=('Microsoft YaHei', 24, 'bold'), fg="#222", bg="#fff", bd=0, highlightthickness=0, relief="flat")
        title_label.pack(pady=(12, 6))
        title_label.bind("<Button-1>", self.on_title_click)
        subtitle_label = Label(card, text="手机扫码上传文件到电脑", font=('Microsoft YaHei', 13), fg="#b0b0b0", bg="#fff", bd=0, highlightthickness=0, relief="flat")
        subtitle_label.pack(pady=(0, 22))

        # 路径选择
        path_frame = Frame(card, bg="#fff")
        path_frame.pack(fill=X, pady=(0, 18), padx=24)
        self.path_btn = Button(path_frame, text="保存路径", command=self.choose_path, font=('Microsoft YaHei', 12), fg="#fff", bg="#1976d2", activebackground="#125ea2", activeforeground="#fff", bd=0, relief=FLAT, padx=16, pady=5, cursor="hand2")
        self.path_btn.pack(side=LEFT)
        self.path_btn.bind("<Enter>", lambda e: self.path_btn.config(bg="#125ea2"))
        self.path_btn.bind("<Leave>", lambda e: self.path_btn.config(bg="#1976d2"))
        self.path_label = Label(path_frame, textvariable=self.save_path, font=('Microsoft YaHei', 12), fg="#1976d2", bg="#f0f4ff", bd=0, highlightthickness=1, relief="solid")
        self.path_label.pack(side=LEFT, padx=(12, 0), ipadx=16, ipady=5, fill='y')
        self.path_label.configure(highlightbackground="#e0e0e0", highlightcolor="#bdbdbd", borderwidth=1)

        # 二维码按钮区
        qr_btn_frame = Frame(card, bg="#fff")
        qr_btn_frame.pack(pady=(0, 14))
        btn_style = {
            'font': ('Microsoft YaHei', 11, 'bold'),
            'bg': '#e0e0e0',
            'fg': '#1976d2',
            'bd': 0,
            'relief': FLAT,
            'padx': 18,
            'pady': 8,
            'cursor': 'hand2',
            'activebackground': '#bdbdbd',
            'activeforeground': '#1976d2',
            'highlightthickness': 0,
        }
        self.local_btn = Button(qr_btn_frame, text="局域网二维码", command=self.show_local_qr, **btn_style)
        self.local_btn.pack(side=LEFT, padx=10)
        self.public_btn = Button(qr_btn_frame, text="公网二维码", command=self.show_public_qr, **btn_style)
        self.public_btn.pack(side=LEFT, padx=10)
        self.p2p_btn = Button(qr_btn_frame, text="P2P直连", command=self.show_p2p_qr, **btn_style)
        self.p2p_btn.pack(side=LEFT, padx=10)
        # 悬停高亮
        def on_enter(btn, bg, fg):
            btn.config(bg=bg, fg=fg)
        def on_leave(btn, is_active):
            if is_active:
                btn.config(bg="#43b984", fg="#fff")
            else:
                btn.config(bg="#e0e0e0", fg="#1976d2")
        self.local_btn.bind("<Enter>", lambda e: on_enter(self.local_btn, "#43b984", "#fff"))
        self.local_btn.bind("<Leave>", lambda e: on_leave(self.local_btn, self.qr_mode.get() == "本地局域网"))
        self.public_btn.bind("<Enter>", lambda e: on_enter(self.public_btn, "#1976d2", "#fff"))
        self.public_btn.bind("<Leave>", lambda e: on_leave(self.public_btn, self.qr_mode.get() == "公网二维码"))
        self.p2p_btn.bind("<Enter>", lambda e: on_enter(self.p2p_btn, "#388E3C", "#fff"))
        self.p2p_btn.bind("<Leave>", lambda e: on_leave(self.p2p_btn, self.qr_mode.get() == "P2P直连"))

        # 二维码卡片区
        qr_card = Frame(card, bg="#fff", bd=0, relief="flat", highlightbackground="#e0e0e0", highlightthickness=1)
        qr_card.pack(pady=(8, 18))  # 上方留白加大，下方保持舒适
        self.qr_label = Label(qr_card, bg="#fff", bd=0, relief=FLAT)
        self.qr_label.pack(ipadx=22, ipady=22)
        self.qr_label.bind("<Enter>", lambda e: self.show_tip("双击二维码可复制图片"))
        self.qr_label.bind("<Leave>", lambda e: self.show_tip(""))
        self.qr_label.bind("<Double-Button-1>", lambda e: self.copy_qr_image())
        self.qr_label['cursor'] = 'hand2'
        # 文件列表卡片区
        self.files_frame = Frame(card, bg="#fff")
        self.files_frame.pack(fill=X, pady=(0, 10), padx=18)
        # 链接Label紧贴卡片底部，宽度与卡片一致
        self.qr_url_label = Label(self.master, textvariable=self.current_qr_url, font=('Microsoft YaHei', 12, 'bold'), fg='#1976d2', bg='#eaf3ff', bd=0, relief='flat', anchor='center', padx=0, pady=10, cursor='hand2', highlightthickness=0, wraplength=420)
        self.qr_url_label.place(relx=0.5, y=card.winfo_y()+card.winfo_height()+8, anchor='n', width=440)
        self.master.after(200, lambda: self.qr_url_label.place_configure(y=self.card.winfo_y()+self.card.winfo_height()+8))  # 确保布局刷新后位置正确
        self.qr_url_label.bind("<Enter>", lambda e: self.show_tip("点击可复制链接"))
        self.qr_url_label.bind("<Leave>", lambda e: self.show_tip(""))
        self.qr_url_label.bind("<Button-1>", lambda e: self.copy_qr_url())

        # 文件数量Label和文件名列表继续在链接Label下方
        self.file_count_var = StringVar()
        self.file_count_label = Label(
            self.master,
            textvariable=self.file_count_var,
            font=("Microsoft YaHei", 12, "bold"),
            fg="#1976d2",
            bg="#fff",
            anchor='center',         # 居中
            justify='center',        # 居中
            pady=4
        )
        self.file_count_label.place(relx=0.5, rely=0.92, anchor='n', width=420)
        self.files_frame = Frame(self.master, bg="#fff")
        self.files_frame.place(relx=0.5, rely=0.96, anchor='n', width=420)

        self.show_local_qr()
        self.master.after(1000, self.check_upload)
        self.master.after(1000, self.check_public_mode_status)
        self.update_qr_btn_highlight()

    def on_title_click(self, event):
        import time
        now = time.time()
        if now - self.title_last_click_time > 5:
            self.title_click_count = 0
        self.title_click_count += 1
        self.title_last_click_time = now
        if self.title_click_count >= 5:
            self.open_settings_window()
            self.title_click_count = 0

    def open_settings_window(self):
        SettingsWindow(self)

    def load_authtoken(self):
        authtoken_path = get_data_file_path('authtoken.txt')
        try:
            with open(authtoken_path, 'r', encoding='utf-8') as f:
                token = f.read().strip()
                if token:
                    return token
        except FileNotFoundError:
            return None
        return None

    def load_token_pool(self):
        """加载token池"""
        tokens_file = get_data_file_path('tokens.txt')
        tokens = []
        try:
            with open(tokens_file, 'r', encoding='utf-8') as f:
                for line in f:
                    t = line.strip()
                    if t and not t.startswith('#'):
                        tokens.append(t)
        except FileNotFoundError:
            pass
        return tokens

    def wait_for_public_url(self, timeout=20):
        if timeout <= 0:
            self.status_label.config(fg="#E53935")
            self.upload_status.set("获取公网地址超时")
            self.cpolar_mgr.stop()
            return
        url = self.cpolar_mgr.get_public_url()
        if url:
            self.qr_mode.set("公网二维码")
            self.update_qr_btn_highlight()
            self.show_qr_img(url)
            self.last_activity_time = time.time()
        else:
            self.master.after(500, lambda: self.wait_for_public_url(timeout - 1))

    def check_public_mode_status(self):
        """公网模式下，更新倒计时或在超时后自动断开"""
        if self.qr_mode.get() == "公网二维码":
            # 仅当成功获取URL后才开始倒计时
            if self.cpolar_mgr.get_public_url():
                idle_time = time.time() - self.last_activity_time
                if idle_time > 600:  # 10分钟超时
                    self.show_tip("公网连接因10分钟无操作已自动断开")
                    self.upload_status.set("公网连接因10分钟无操作已自动断开")
                    self.status_label.config(fg="#E53935", bg="#ffeaea", font=('Microsoft YaHei', 17, 'bold'))
                    self.show_local_qr()
                else:
                    remaining_time = 600 - int(idle_time)
                    self.upload_status.set(f"公网连接中 (剩余 {remaining_time // 60}:{remaining_time % 60:02d})")
                    self.status_label.config(fg="#1976d2", bg="#eaf3ff", font=('Microsoft YaHei', 17, 'bold'))
        self.master.after(1000, self.check_public_mode_status)
    
    def check_upload(self):
        global uploaded_filenames
        history_changed = False
        if uploaded_filenames:
            # 任何上传都会重置公网模式的计时器
            if self.qr_mode.get() == "公网二维码":
                self.last_activity_time = time.time()
                self.flash_message(f"{len(uploaded_filenames)}个文件上传成功!", duration=3000)

            self.upload_history.extend(uploaded_filenames)
            uploaded_filenames = []
            history_changed = True
            
        if self.upload_history != self.last_history:
            history_changed = True
            
        if history_changed:
            for widget in self.files_frame.winfo_children():
                widget.destroy()
            if self.upload_history:
                self.file_count_var.set(f"收到 {len(self.upload_history)} 个文件：")
                for idx, fname in enumerate(self.upload_history):
                    label = Label(self.files_frame, text=fname, font=("PingFang SC", 11), bg="#f7f7f7", anchor=W, padx=10, pady=6, bd=0, relief=FLAT)
                    label.pack(fill=X, pady=(0, 2))
            else:
                self.file_count_var.set("")
            self.last_history = list(self.upload_history)
            
        self.master.after(1000, self.check_upload)

    def flash_message(self, msg, duration=2000):
        """在顶部提示栏闪烁一条临时消息"""
        original_msg = self.tip_var.get()
        self.tip_var.set(msg)
        self.master.after(duration, lambda: self.tip_var.set(original_msg))

    def show_local_qr(self):
        self.cpolar_mgr.stop()
        self.qr_mode.set("本地局域网")
        self.update_qr_btn_highlight()
        ip = get_local_ip()
        url = f"http://{ip}:{PORT}/"
        self.show_qr_img(url)
        self.upload_status.set("当前为局域网模式")
        self.status_label.config(fg="#388E3C", bg="#eafaea", font=('Microsoft YaHei', 17, 'bold'))

    def show_public_qr(self):
        authtoken = self.load_authtoken()
        if authtoken:
            self.cpolar_mgr.start(authtoken)
            self.last_activity_time = time.time()
            self.status_label.config(fg="#FF9800", bg="#eaf3ff", font=('Microsoft YaHei', 17, 'bold'))
            self.upload_status.set("正在获取公网地址...")
            self.master.update_idletasks()
            self.wait_for_public_url()
            return
        # authtoken为空，尝试token池
        if self.token_pool:
            self.token_index = 0
            self.try_next_token_in_pool()
            return
        self.show_tip("未找到可用token! 请在设置中提供cpolar authtoken或在tokens.txt中添加token")
        self.open_settings_window()

    def try_next_token_in_pool(self):
        if self.token_index >= len(self.token_pool):
            # 本地token池全部无效，尝试拉取最新tokens.txt
            self.show_tip("本地token全部失效，正在尝试下载最新token池...")
            auto_update_tokens_from_url()
            # 重新加载token池
            self.token_pool = self.load_token_pool()
            self.token_index = 0
            if not self.token_pool:
                self.show_tip("所有token均不可用，请在设置中提供cpolar authtoken！")
                self.status_label.config(fg="#E53935")
                self.upload_status.set("获取公网地址失败")
                self.open_settings_window()  # 自动打开设置窗口让用户设置authtoken
                return
            # 拉取后重试
            self.try_next_token_in_pool()
            return
        token = self.token_pool[self.token_index]
        self.cpolar_mgr.start(token)
        self.status_label.config(fg="#FF9800")
        self.upload_status.set(f"正在尝试第{self.token_index+1}个token获取公网地址...")
        self.master.update_idletasks()
        self.wait_for_public_url_token_pool(token, timeout=20)

    def wait_for_public_url_token_pool(self, token, timeout=20):
        if timeout <= 0:
            # 当前token超时，尝试下一个
            self.cpolar_mgr.stop()
            self.token_index += 1
            self.try_next_token_in_pool()
            return
        url = self.cpolar_mgr.get_public_url()
        if url:
            self.qr_mode.set("公网二维码")
            self.update_qr_btn_highlight()
            self.show_qr_img(url)
            self.last_activity_time = time.time()
        else:
            self.master.after(500, lambda: self.wait_for_public_url_token_pool(token, timeout - 1))

    def show_qr_img(self, url):
        print(f"[调试] show_qr_img url: {url}")  # 调试输出
        if not url:
            url = "暂无二维码链接"
        qr = qrcode.make(url)
        qr_img = qr.resize((340, 340))
        self.qr_img = qr_img
        self.qr_photo = ImageTk.PhotoImage(qr_img)
        self.qr_label.config(image=self.qr_photo)
        self.qr_label.image = self.qr_photo
        self.current_qr_url.set(url)
        self.qr_url_label.lift()  # 保证Label在最上层

    def get_current_url(self):
        # 此函数在choose_path中被调用，但此时cpolar可能没有启动
        # 它应该只返回当前UI上显示的URL
        return self.current_qr_url.get()

    def show_tip(self, msg):
        self.tip_var.set(msg)

    def copy_qr_url(self):
        self.master.clipboard_clear()
        self.master.clipboard_append(self.current_qr_url.get())
        self.show_tip("已复制")

    def copy_qr_image(self):
        if self.qr_img is None or win32clipboard is None:
            self.show_tip("无法复制二维码图片（依赖缺失）")
            return
        output = io.BytesIO()
        self.qr_img.convert('RGB').save(output, 'BMP')
        data = output.getvalue()[14:]
        output.close()
        try:
            win32clipboard.OpenClipboard()
            win32clipboard.EmptyClipboard()
            win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
            win32clipboard.CloseClipboard()
            self.show_tip("二维码图片已复制")
        except Exception:
            self.show_tip("复制失败，请重试")

    def load_save_path(self):
        """加载上次使用的保存路径"""
        path_file = get_data_file_path('save_path.txt')
        try:
            with open(path_file, 'r', encoding='utf-8') as f:
                path = f.read().strip()
                # 校验路径是否存在，如果不存在则使用默认值
                if os.path.isdir(path):
                    return path
        except FileNotFoundError:
            pass # 文件不存在，使用默认值
        return UPLOAD_FOLDER # 默认路径

    def save_save_path(self, path):
        """保存当前使用的路径"""
        path_file = get_data_file_path('save_path.txt')
        try:
            with open(path_file, 'w', encoding='utf-8') as f:
                f.write(path)
        except Exception as e:
            self.show_tip(f"保存路径设置失败: {e}")

    def choose_path(self):
        path = filedialog.askdirectory()
        if path:
            self.save_path.set(path)
            app.config['UPLOAD_FOLDER'] = path
            self.save_save_path(path)
            self.show_qr_img(self.get_current_url())

    def on_close(self):
        self.cpolar_mgr.stop()
        self.master.destroy()

    def update_qr_btn_highlight(self):
        if self.qr_mode.get() == "本地局域网":
            self.local_btn.config(bg="#43b984", fg="#fff")
            self.public_btn.config(bg="#e0e0e0", fg="#1976d2")
            self.p2p_btn.config(bg="#e0e0e0", fg="#1976d2")
        elif self.qr_mode.get() == "公网二维码":
            self.local_btn.config(bg="#e0e0e0", fg="#1976d2")
            self.public_btn.config(bg="#1976d2", fg="#fff")
            self.p2p_btn.config(bg="#e0e0e0", fg="#1976d2")
        elif self.qr_mode.get() == "P2P直连":
            self.local_btn.config(bg="#e0e0e0", fg="#1976d2")
            self.public_btn.config(bg="#e0e0e0", fg="#1976d2")
            self.p2p_btn.config(bg="#388E3C", fg="#fff")

    def show_p2p_qr(self):
        if RTCPeerConnection is None:
            self.show_tip("未安装aiortc，无法使用P2P直连！请先 pip install aiortc")
            return
        session_id = str(uuid.uuid4())
        # 获取公网地址
        public_url = self.cpolar_mgr.get_public_url()
        if not public_url:
            self.show_tip("请先获取公网地址（点击公网二维码），再用P2P直连！")
            return
        url = f"{public_url}/p2p?session={session_id}"
        self.qr_mode.set("P2P直连")
        self.update_qr_btn_highlight()
        self.show_qr_img(url)
        self.upload_status.set("P2P直连模式，扫码建立点对点连接")
        self.status_label.config(fg="#43b984", bg="#eafaea", font=('Microsoft YaHei', 17, 'bold'))
        # 启动P2P接收服务
        P2PReceiver(session_id, self.save_path.get(), status_callback=self.show_p2p_status)

    def show_p2p_status(self, msg, color="#1976d2"):
        # 彻底去除倒计时相关内容
        if "倒计时" in msg or "剩余" in msg:
            return  # 忽略带倒计时的内容
        self.upload_status.set(msg)
        self.status_label.config(fg=color)

    def clear_p2p_status(self):
        self.p2p_status_active = False
        if self.p2p_status_timer:
            self.master.after_cancel(self.p2p_status_timer)
            self.p2p_status_timer = None
        # 恢复为当前模式的正常提示
        if self.qr_mode.get() == "本地局域网":
            self.upload_status.set("当前为局域网模式")
            self.status_label.config(fg="#388E3C", bg="#eafaea", font=('Microsoft YaHei', 17, 'bold'))
        elif self.qr_mode.get() == "公网二维码":
            self.upload_status.set("正在获取公网地址...")
            self.status_label.config(fg="#FF9800", bg="#eaf3ff", font=('Microsoft YaHei', 17, 'bold'))
        else:
            self.upload_status.set("")
            self.status_label.config(fg="#1976d2", bg="#eaf3ff", font=('Microsoft YaHei', 17, 'bold'))

# ========== 启动主程序 ==========
def main():
    # 确保首次运行时cpolar.exe被安置好
    ensure_cpolar_in_data_dir()
    # 确保首次运行时tokens.txt被安置好
    ensure_tokens_file_in_data_dir()
    auto_update_tokens_from_url()  # 启动时自动拉取Gitee tokens.txt
    flask_thread = threading.Thread(target=run_flask, daemon=True)
    flask_thread.start()
    root = Tk()
    app_gui = QRFileTransferApp(root)
    root.protocol("WM_DELETE_WINDOW", app_gui.on_close)
    try:
        root.mainloop()
    finally:
        app_gui.cpolar_mgr.stop()

if __name__ == '__main__':
    main()